<?php

/**
 * Acl model
 *
 * @author     Mon Zafra <monzee at gmail>
 * @copyright  (c)2009 Mon Zafra
 * @category   Mz
 * @package    modules
 * @subpackage admin
 * @license    http://mz-project.googlecode.com/svn/trunk/LICENSE    MIT License
 * @version    SVN: $Id: Acl.php 47 2009-05-28 17:56:56Z monzee $
 */
class Admin_Model_Acl
{
    protected $_adapter;
    static protected $_adapterPrefix = 'Admin_Model_Acl';
    protected $_defaultAdapterClass = 'Admin_Model_Acl_Array';
    protected $_resourceIdSpec = ':module/:controller';
    protected $_resourcePrivSpec = ':action';

    protected $_resource;
    protected $_roles = array();
    protected $_parentRoles = array();
    protected $_userRoles = array();
    protected $_rules = array();

    /**
     * Constructor.
     *
     * See constructors for adapter classes for possible option keys.
     *
     * @param string|Admin_Model_Acl_Adapter $adapter Acl storage adapter
     * @param array|Zend_Config              $options Options to pass to adapter
     *                                                constructor
     */
    public function __construct($adapter = null, $options = array())
    {
        if (!empty($adapter) && is_string($adapter)) {
            $adapter = self::factory($adapter, $options);
        }
        if (!empty($adapter)) {
            $this->setAdapter($adapter);
        }
    }

    /**
     * Instantiate adapter.
     *
     * @param string            $adapterClass
     * @param array|Zend_Config $options
     * @return Admin_Model_Acl_Adapter
     */
    static public function factory($adapterClass, $options = array())
    {
        $class = self::$_adapterPrefix . '_' . ucfirst($adapterClass);
        $adapter = new $class($options);
        return $adapter;
    }

    /**
     * Set prefix of adapter (used by factory).
     *
     * @param string $prefix
     */
    static public function setAdapterPrefix($prefix)
    {
        self::$_adapterPrefix = rtrim($prefix, '_');
    }

    /**
     * Lazy load adapter
     *
     * @return Admin_Model_Acl_Adapter
     */
    public function getAdapter()
    {
        if (null === $this->_adapter) {
            $this->_adapter = new $this->_defaultAdapterClass;
        }
        return $this->_adapter;
    }

    /**
     * Set storage adapter.
     *
     * @param Admin_Model_Acl_Adapter $adapter
     * @return Admin_Model_Acl
     */
    public function setAdapter(Admin_Model_Acl_Adapter $adapter)
    {
        $this->_adapter = $adapter;
        return $this;
    }

    /**
     * Param $roles must be an iteratable set of roles indexed by username.
     * Roles must be an array (or object with ArrayAccess interface) that
     * contains at least an 'id' field.
     *
     * e.g. setUserRoles(
     *   array('foo' => array(
     *     array('id' => 'author'),
     *     array('id' => 'moderator'),
     *   ),
     * ));
     *
     * @param string $user  The user
     * @param mixed  $roles His roles
     * @return Admin_Model_Acl
     */
    public function setUserRoles($roles)
    {
        $this->_userRoles = $roles;
        return $this;
    }

    /**
     * Add roles
     * 
     * @param string $user Username
     * @param mixed  $roles 
     * @return Admin_Model_Acl
     */
    public function addUserRole($user, $roles)
    {
        $this->_userRoles[$user] = $roles;
        return $this;
    }

    /**
     * Set resource id and privilege spec.
     * 
     * Both strings can contain the strings ':module', ':controller', and
     * ':action' which will be replaced with the corresponding request param
     * values.
     * 
     * @param string $id
     * @param string $priv
     * @return Admin_Model_Acl
     */
    public function setResourceSpec($id = ':module/:controller', $priv = ':action')
    {
        $this->_resourceIdSpec = $id;
        $this->_resourcePrivSpec = $priv;
        return $this;
    }

    /**
     * $resource must be an array containing the resource id and privilege
     *
     * e.g. array('id' => 'blog/entry', 'privilege' => 'create')
     *
     * @param mixed $resource
     * @return mixed
     * @throws Admin_Model_Acl_Exception if the resource doesn't have an id key
     */
    public function setResource($resource)
    {
        $this->_resource = is_array($resource) 
                         ? $resource : array('id' => $resource);
        if (!array_key_exists('id', $resource)) {
            throw new Admin_Model_Acl_Exception("A resource must have at least an 'id' key");
        }
        return $this;
    }

    /**
     * Returns resource id and privilege.
     *
     * $params may be null or an array with the keys 'module', 'controller'
     * and 'action'. If it is null, the info is pulled from the current request.
     *
     * @param null|array $params
     * @return array ('id' => resId, 'privilege' => priv)
     * @throws Admin_Model_Acl_Exception If a key is missing
     */
    public function getResource(array $params = null)
    {
        if (null !== $this->_resource) {
            return $this->_resource;
        }
        if (null !== $params) {
            $keys = array_keys($params);
            $comp = array('module', 'controller', 'action');
            if ($comp != array_intersect($comp, $keys)) {
                throw new Admin_Model_Acl_Exception('Resource params must have a module, controller and action.');
            }
        }
        return $this->_generateResource($params);
    }

    /**
     * Proxy to the acl model adapter, cache results
     *
     * @return mixed
     */
    public function getUserRolesByUsername($username)
    {
        if (!isset($this->_userRoles[$username])) {
            $adapter = $this->getAdapter();
            $user = $adapter->getUserByUsername($username);
            $roles = $adapter->getUserRoles($user);
            $userRoles = array();
            foreach ($roles as $role) {
                $userRoles[] = $role['id'];
            }
            $this->_userRoles[$username] = $userRoles;
        }
        return $this->_userRoles[$username];
    }

    /**
     * Proxy to acl model adapter
     *
     * @param mixed $roleId
     * @return mixed
     */
    public function getRoleById($roleId)
    {
        if (!isset($this->_roles[$roleId])) {
            $this->_roles[$roleId] = $this->getAdapter()->getRoleById($roleId);
        }
        return $this->_roles[$roleId];
    }

    /**
     * Proxy to acl model adapter
     *
     * @param array $roleId Role id
     * @return mixed Array of role ids
     */
    public function getParentRoles($roleId)
    {
        if (!isset($this->_parentRoles[$roleId])) {
            $role = $this->getRoleById($roleId);
            $parents = $this->getAdapter()->getParentRoles($role);
            $parentRoles = array();
            foreach ($parents as $parent) {
                $parentRoles[] = $parent['id'];
            }
            $this->_parentRoles[$roleId] = $parentRoles;
        }
        return $this->_parentRoles[$roleId];
    }

    /**
     * Proxy to acl model adapter
     *
     * @param string $resId Resource id to find
     * @return mixed
     */
    public function getRulesByResourceId($resId)
    {
        if (!isset($this->_rules[$resId])) {
            $this->_rules[$resId] = $this->getAdapter()->getRulesByResourceId($resId);
        }
        return $this->_rules[$resId];
    }

    /**
     * Generate resource id and priv
     *
     * $params must contain the keys 'module', 'controller' and 'action'
     *
     * @param null|array $params Uses the current request if null
     * @return array containing resourceId and privilege
     * @throws Admin_Model_Acl_Exception If params has missing keys
     */
    protected function _generateResource(array $params = null)
    {
        if (null !== $params) {
            $module = $params['module'];
            $controller = $params['controller'];
            $action = $params['action'];
        } else {
            $req = Zend_Controller_Front::getInstance()->getRequest();
            $module = $req->getModuleName();
            $controller = $req->getControllerName();
            $action = $req->getActionName();
        }

        $replaceMap = array(
            ':module' => $module,
            ':controller' => $controller,
            ':action' => $action,
        );

        $resId = str_replace(
            array_keys($replaceMap),
            array_values($replaceMap),
            $this->_resourceIdSpec
        );

        $priv = str_replace(
            array_keys($replaceMap),
            array_values($replaceMap),
            $this->_resourcePrivSpec
        );

        return array($resId, $priv);
    }

    /**
     * Proxy other calls to adapter
     * 
     * @param string $method
     * @param array  $args
     * @return mixed
     */
    public function __call($method, $args)
    {
        $adapter = $this->getAdapter();
        if (method_exists($adapter, $method)) {
            return call_user_func_array(array($adapter, $method), $args);
        }
        throw new BadMethodCallException('No such method (' . $method . ').');
    }


}
